/*
 * SPDX-License-Identifier: BSD-3-Clause
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2023. All rights reserved.
 * Description   : virtual dev interface
 */

#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

#include "securec.h"

#include "udk_common.h"
#include "udk_spinlock.h"
#include "udk_log.h"

#include "udk_vdev.h"

/* List of virtual device */
TAILQ_HEAD(udk_vdev_device_list, udk_vdev_device);

static struct udk_vdev_device_list udk_vdev_device_list = TAILQ_HEAD_INITIALIZER(udk_vdev_device_list);
static udk_spinlock_t udk_vdev_device_list_lock = UDK_SPINLOCK_INITIALIZER;

int udk_vdev_is_probed(const struct udk_vdev_device *dev)
{
    /* The field driver should be set only when the probe is successful. */
    return dev->driver != NULL;
}

static int udk_vdev_probe_driver(struct udk_vdev_device *dev, struct udk_vdev_driver *driver)
{
    int ret;

    if (udk_vdev_is_probed(dev) != 0) {
        return -EEXIST;
    }

    ret = driver->probe(dev);
    if (ret == 0) {
        dev->driver = driver;
    }
    return ret;
}


/* The caller shall be responsible for thread-safe */
static struct udk_vdev_device *udk_find_vdev(const char *name)
{
    struct udk_vdev_device *dev = NULL;

    if (name == NULL) {
        return NULL;
    }

    TAILQ_FOREACH(dev, &udk_vdev_device_list, next) {
        const char *devname = udk_vdev_device_name(dev);

        if (!strcmp(devname, name)) {
            return dev;
        }
    }

    return NULL;
}

static int udk_init_devargs(struct udk_vdev_device *dev, const char *name, const char *args)
{
    int ret;

    if (args != NULL) {
        dev->args = strdup(args);
    } else {
        dev->args = strdup("");
    }

    ret = strcpy_s(dev->name, sizeof(dev->name), name);
    if (ret != EOK) {
        free(dev->args);
        dev->args = NULL;
        return ret;
    }

    return 0;
}

static int udk_insert_vdev(const char *name, const char *args, struct udk_vdev_device **p_dev)
{
    struct udk_vdev_device *dev = NULL;
    int ret;

    if (name == NULL) {
        return -EINVAL;
    }

    if (udk_find_vdev(name)) {
        /*
         * A vdev is expected to have only one port.
         * So there is no reason to try probing again,
         * even with new arguments.
         */
        return -EEXIST;
    }

    dev = calloc(1, sizeof(*dev));
    if (dev == NULL) {
        return -ENOMEM;
    }

    ret = udk_init_devargs(dev, name, args);
    if (ret != 0) {
        free(dev);
        return -ENOMEM;
    }

    dev->numa_node = UDK_SOCKET_ID_ANY;
    TAILQ_INSERT_TAIL(&udk_vdev_device_list, dev, next);

    if (p_dev != NULL) {
        *p_dev = dev;
    }

    return 0;
}

static void udk_remove_vdev(struct udk_vdev_device *dev)
{
    TAILQ_REMOVE(&udk_vdev_device_list, dev, next);
    free(dev->args);
    free(dev);
}

int udk_vdev_init(const char *name, const char *args, struct udk_vdev_driver *driver)
{
    struct udk_vdev_device *dev = NULL;
    int ret;

    udk_spinlock_lock(&udk_vdev_device_list_lock);
    ret = udk_insert_vdev(name, args, &dev);
    if (ret == 0) {
        ret = udk_vdev_probe_driver(dev, driver);
        if (ret != 0) {
            if (ret > 0) {
                UDK_LOG(ERR, VDEV, "no driver found for %s", name);
            }
            /* If fails, remove it from vdev list */
            udk_remove_vdev(dev);
        }
    }
    udk_spinlock_unlock(&udk_vdev_device_list_lock);
    return ret;
}

static int udk_vdev_remove_driver(struct udk_vdev_device *dev)
{
    if (!dev->driver) {
        UDK_LOG(DEBUG, VDEV, "no driver attach to device %s", dev->name);
        return -EINVAL;
    }

    return dev->driver->remove(dev);
}


int udk_vdev_uninit(const char *name)
{
    struct udk_vdev_device *dev = NULL;
    int ret;

    if (name == NULL) {
        return -EINVAL;
    }

    udk_spinlock_lock(&udk_vdev_device_list_lock);

    dev = udk_find_vdev(name);
    if (dev == NULL) {
        ret = -ENOENT;
        goto unlock;
    }

    ret = udk_vdev_remove_driver(dev);
    if (ret != 0) {
        goto unlock;
    }

    udk_remove_vdev(dev);

unlock:
    udk_spinlock_unlock(&udk_vdev_device_list_lock);
    return ret;
}
